Repository navigation
[OMNIML-6085] Run expert-parallel PTQ from hf_ptq.py - #2754
shengliangxu wants to merge 57 commits into
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review: 3
Please check the settings in the CodeRabbit UI or the
|
| Layer / File(s) | Summary |
|---|---|
Distribution planning and model loading examples/hf_ptq/hf_ptq.py, examples/hf_ptq/example_utils.py, tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py, tests/examples/hf_ptq/test_hf_ptq_args.py, tests/examples/hf_ptq/test_hf_ptq_vision_quantization.py, examples/hf_ptq/README.md, CHANGELOG.rst |
The example resolves distribution plans, checks supported layouts, and passes plans to pretrained-loading paths. The guide describes expert-parallel configuration and launch options. Tests cover plan selection and loading behavior. |
Distributed execution and calibration examples/hf_ptq/hf_ptq.py, examples/hf_ptq/example_utils.py, modelopt/torch/utils/distributed.py, tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py, tests/examples/hf_ptq/test_llm_ptq.py, tests/unit/torch/utils/test_distributed_calibration.py |
The run uses a distributed context with a timeout above 30 minutes. Supported calibration data loaders are sharded across ranks, and expert-parallel plans use distributed_forward_loop. The integration test runs two-rank Qwen3-MoE PTQ and checks the exported FP8 expert weights and scales. |
Synchronized previews and checkpoint export examples/hf_ptq/example_utils.py, examples/hf_ptq/hf_ptq.py, examples/hf_ptq/vlm_utils.py, tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py, tests/examples/hf_ptq/test_hf_ptq_model_type.py |
Generation kwargs enable synchronized previews for sharded models. Replicated models export on rank 0; dense-FSDP and expert-parallel models export on all ranks. Tests cover preview kwargs and export participation. |
NVML memory probe
| Layer / File(s) | Summary |
|---|---|
NVML probe lifecycle modelopt/torch/utils/memory_monitor.py, tests/unit/torch/utils/test_memory_monitor.py |
The memory monitor initializes NVML before probing GPU memory and shuts it down after the probe. Tests cover supported and unsupported memory reporting. |
Priority:
Estimated code review effort: 4 (Complex) | ~45 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
participant Ranks as torchrun ranks
participant PTQ as HF PTQ
participant Loader as load_pretrained
participant Data as Calibration loader
participant Forward as distributed_forward_loop
participant Export as Checkpoint export
Ranks->>PTQ: Resolve distribution plan
PTQ->>Loader: Load model with distribution plan
PTQ->>Data: Shard calibration data across ranks
Data->>Forward: Provide rank-local calibration batches
Forward->>Export: Complete expert-parallel calibration
Ranks->>Export: Export checkpoint shards
Merge Risk: 
Minimal · previous review 78f56
-
No confirmed issue blocks merging. The distributed speculative-decoding calibration path remains unverified.
Pre-merge checks: 
5
-
Full details: Security Anti-Patterns
-
Explanation
No CRITICAL security anti-pattern was introduced. The changed
modeloptandexamplesPython code adds no unsafetorch.load,numpy.load(..., allow_pickle=True),eval/exec, or# nosecbypass. Transformer loading receives caller-controlledtrust_remote_codevalues, with defaults set toFalse; the literalTruevalues occur only in newly added test fixtures. The pull request changes nopyproject.tomlor requirements file.
-
✨ Finishing Touches: 💡 2
-
📝 Generate docstrings: 💡
-
- Commit to this branch
- Create a new PR
🛠️ Fix failing CI checks: 💡
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
b298b8a to
9364c1b
Compare
4e11e09 to
be3cc39
Compare
9364c1b to
9d9b154
Compare
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## shengliangx/hfep-11c-kimi-k3 #2754 +/- ##
================================================================
+ Coverage 62.63% 62.69% +0.06%
================================================================
Files 668 668
Lines 74485 74490 +5
================================================================
+ Hits 46654 46703 +49
+ Misses 27831 27787 -44
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/ok to test 9d9b154 |
be3cc39 to
bdf08a2
Compare
9d9b154 to
487a8e8
Compare
|
/ok to test 487a8e8 |
487a8e8 to
c8d0e4f
Compare
136543b to
405ef82
Compare
c8d0e4f to
22ac8fc
Compare
|
/ok to test 22ac8fc |
22ac8fc to
d2b13df
Compare
d2b13df to
f9293cd
Compare
f9293cd to
3be65ef
Compare
…FSDP2 The two-GPU hf_ptq test skipped generation, so no test reached a sharded preview. A real Qwen3-30B-A3B run did: under FSDP2 Transformers refused the experts switch generate makes for an FSDP2-wrapped class, after the previews of an earlier round had hung ranks that finished decoding at different steps. The test now runs both plans, expert parallelism and FSDP2, with the previews on, on a tiny Qwen3-MoE. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
hf_ptq refused the TensorRT-LLM exporter on a multi-rank plan but not AutoQuantize, whose search does not run through distributed_forward_loop and scores each rank's own calibration shard without reducing the scores across ranks. Ranks could choose different formats per layer, which under expert parallelism writes a checkpoint of mixed formats. A multi-rank run now refuses the recipe before the load; --use_fsdp2 keeps main's warning for it, and a single process runs it as before. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…run whose preview fails Under torchrun each rank pins the whole model to its own GPU, and every load path now honours the pin: the pack-quantized and MXFP4 paths placed by maps of their own, and DiffusionGemma asked for a sequential map, so every rank's copy landed on the same GPUs. An explicit --use_seq_device_map, which spreads one model over every GPU, is refused where each rank holds its own copy. A preview that fails on a sharded model raises instead of being skipped: its ranks decode in lockstep, so a rank that walked on to the export left the others blocked in the next step's collective; raising aborts the run. The Nemotron-VL text and image previews do the same. The offline speculative-decoding loader is split across ranks like the others, and a loader of a kind that cannot be split is refused instead of fed whole to every rank. The load tests read the spec's plan off the decision's copy of the config, which now carries it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Each node reads the VL preview's sample images from its own checkout, and the repository ships none, so the ranks could disagree on whether there is one. A rank without an image returned before generating while its peers decoded in lockstep, which left them blocked in the next step's collective until the process-group timeout. The ranks of a sharded model now decide together: every rank previews, or none does. Raising instead would stop every sharded Nemotron-VL run on a checkout without images. A failed preview that stops the run now says the export was not reached and that --skip_generate exports without the preview, with the preview's error as its cause. The GPU memory monitor reads every GPU on the node, so rank 0 starts it rather than every rank. _resolve_distribution no longer refuses FSDP2 within several expert-data-parallel replicas itself: no plan from resolve_distribution_plan reaches that check, since resolve_distribution_plan refuses the layout first, and load_and_prepare_ep keeps its own refusal for a plan handed to it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An expert-parallel run never reaches get_model or the low-memory loader, so it accepted --low_memory_mode, --use_seq_device_map and --offload_folder and ignored them: torchrun with --low_memory_mode loaded an MoE at full precision without a word. A replicated run could not use them either. Its pin of each rank's copy to the rank's GPU leaves --offload_folder nothing to offload, and the low-memory loader cannot run under a process group: it all-reduces amax computed from meta weights, and hands accelerate its own device map next to the pin. Both routes now refuse the three options before loading, as they refuse the TensorRT-LLM exporter and AutoQuantize. A single process and --use_fsdp2 keep main's handling. get_model no longer sizes a pinned copy's memory over the node's GPUs: the pinned map ignores max_memory, so the limit it printed was never applied. The README no longer points GPT-OSS and Llama-4 at --use_fsdp2, which refuses their released checkpoints (an MXFP4 quantization_config, a multimodal config); it says to quantize them in a single process. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An expert-parallel run that quantized without running the calibration loop lost its experts' all-to-all forwards: quantizer insertion drops them, and only distributed_forward_loop re-planted them, before its first forward. A config without static activation scales (int8_weight_only, w4a16_nvfp4, ...) calibrates without the loop, and algorithm None (the mxfp4, mxfp6, mxfp8 and mxint8 presets) never calls the loop it is handed, so the preview after PTQ raised "torch_a2a experts forward requires the EP process_group" and stopped the run before the export. mono_quantize now re-plants them once the model is quantized; after a loop that ran, that is a no-op. An expert-parallel run refuses --vllm_fakequant_export before the load. That exporter writes the whole model with save_pretrained from each process it runs in, and every rank of a sharded model exports, so each rank would write its own share of the experts into the same directory. Rank 0 alone still writes a replicated model. keep_the_experts_implementation puts back an _optimize_model_for_decode the model held itself instead of deleting it, so a hook bound per instance survives the preview and the manager nests. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The hf_ptq README said a model needing a newer transformers says so before loading, and described auto's EP degree without the floor; since [13/21], auto resolves to 1 there and the run is refused by the plan instead. _reinstall_a2a_forwards' docstring named the calibration loop as its only caller; hf_ptq now calls it after quantizing too. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The preview after quantization raised on a failure whenever the plan sharded the model, which aborts the run before the export, for --use_fsdp2 as well as for expert parallelism. On main a failed --use_fsdp2 preview warned and the export ran, and a failure every rank meets at the same step (an unsupported kernel, the case that skip was written for) still exports that way. Only an expert-parallel preview now stops the run; --use_fsdp2 keeps main's skip. get_model pinned a rank's copy to its GPU only for a device with an index, and under a multi-rank plan fell back to "auto" for one without, the shared-GPU placement its comment warns about. It now refuses such a device; hf_ptq hands every rank of a launch an indexed cuda:<local_rank>. The re-plant after quantizing notes it matters from transformers 5.16: earlier releases install the TP transforms as forward hooks, which quantizer insertion leaves in place. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An expert-parallel run that calibrated a VLM on text handed distributed_forward_loop the language model, and the loop re-plants the all-to-all forwards over the model it is handed. transformers keeps the expert-parallel plan on the model from_pretrained built: from 5.16, tp_plan returns it only for a module whose own config carries distributed_config, which from_pretrained sets on the top config alone. The language model's plan named no experts, so the forwards quantizer insertion drops stayed dropped, and the first batch raised "torch_a2a experts forward requires the EP process_group". The loop now takes the whole model, as it already did for image batches, and runs text batches through the language model; quantization still targets the language model alone. A CPU test builds a tiny Qwen3-VL-MoE as the expert-parallel load leaves it and calibrates it on text over a one-rank gloo group; without the change it fails on transformers 5.18. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
keep_the_experts_implementation planted its stand-in for generate's _optimize_model_for_decode on the model it was handed only. A Nemotron-VL model's chat generates through its language model, whose generate then entered its own hook and switched the experts to batched_mm: under expert parallelism that forward is not the torch_a2a one and raises, which stops the run before the export; otherwise the preview after PTQ decoded on unquantized experts. The guard now covers every module of the model that has the hook, and puts back what each one held. The post-PTQ preview's comment also says what a failure on one --use_fsdp2 rank alone does: the other ranks stay blocked in generate's collectives until the collective timeout. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ngelog hf_ptq now runs inside distributed_run, which refuses a device other than cuda under a launch; on main a multi-rank torchrun ignored --device. Record that in Backward Breaking Changes. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
export_speculative_decoding writes the whole checkpoint from one process and knows no plan, so under an expert-parallel or FSDP2 plan every rank reached it and wrote the same files over each other. export_quantized now refuses that export for a sharded model, on every rank alike, before writing anything; a replicated model still exports it from rank 0. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel load now requires 5.7, the first release that builds a model with torch_a2a. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
pre_quantize called full_model.generate for every model it did not skip by type, and a remote-code class that does not inherit GenerationMixin has no generate: Kimi-K3's KimiK3ForConditionalGeneration stopped the run there with an AttributeError, before quantization, unless --skip_generate was passed. Such a model now skips the preview, before quantization and so after it too. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The skip was for Kimi-K3, whose model had no generate, but only rank 0's lacked it: the plan decision's meta build had registered the remote class with the auto class, so rank 0's load built it as registered, without the GenerationMixin every other rank's first load adds. [5/21] now undoes that registration, and every rank's Kimi-K3 has generate. The skip, decided on each rank alone, instead let rank 0 move on to calibration while the others decoded in lockstep, a deadlock where the missing method would have raised. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
[13/21] holds every expert-parallel spec to the expert-parallel load's transformers floor, so on transformers 5.5, the CI floor, a multi-rank DeepSeek-V3 run is refused before the hf_ptq route these cases exercise. They skip below the floor; their single-process and --use_fsdp2 cases still run there. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The preview prints the transcript of the set's first sample next to each rank's first output, so a sharded loader paired every rank but rank 0 with another sample's transcript. With the amax synced across the ranks, the calibration comes out the same as from shards. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
It sat third, so a caller passing gpu_mem_percentage or a later option by position handed it to distribution instead. Every caller in the tree passes it by keyword, as they keep doing. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…he guard It does not: the plain loop runs under the same guard, so the distributed loop restores nothing there. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ent tests Main replaced the FSDP2 KV-AutoQuantize refusal test this PR had pointed at transformers.AutoConfig, so nothing in the file uses the import any more. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Main's new test checks that an FSDP2 KV-AutoQuantize run reaches the distributed load by stubbing parallel_load_and_prepare_fsdp2, which hf_ptq no longer calls: it loads every distributed plan through load_pretrained. The test stubs that instead, as its MTP neighbour does. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
On a launch of several ranks, deciding the plan builds the model on the meta device, also when --use_fsdp2 pins both knobs. Some checkpoints validate_fsdp2_supported refuses (VILA, multimodal, pack-quantized) cannot be built there, and that build's error names neither --use_fsdp2 nor the way out. The checks read the config alone, so they now run first, as they did before the run decided its plan. The DeepSeek-V3 test config names its architecture, as a checkpoint's does, since the checks read it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The check compared the device with the string "cuda", so a replicated
run, whose ranks pass torch.device("cuda:<local_rank>"), never printed
the warning, though a pinned copy that accelerate spilled to the CPU is
where it matters most. It now compares the device type.
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A run of several ranks never calibrates Whisper: _resolve_distribution refuses every encoder-decoder model the TensorRT-LLM exporter takes, Whisper included, before the load. The exemption and its test covered a configuration no launch reaches. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
It reads every GPU on its node, so one per rank polls them all once per rank, but gating it on global rank 0 left every node but the first unreported on a multi-node run, where expert-parallel ranks hold different experts. Each node's local rank 0 starts it now. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Per-layer export writes the same shard files from every rank. assert_layerwise_export_supported refuses that, but only once calibration binds the exporter, after the expert-parallel load placed the model on every rank. _resolve_distribution refuses it before the load, as it does the other single-process-only options. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The Nemotron VL load replaces the device map with None and moves the model to the rank's GPU only at the end, through host memory, so not every path below honours the pin as the comment said. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The note read as the opposite of what it meant. The loads with no device map (Nemotron VL) skip the pin: each rank builds a whole copy in host memory before moving it to its GPU, so a node needs one host copy per rank. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal before a distributed load and quantize_main's export switch each spelled out the test for a layerwise block that exports, and only the two staying identical kept the refusal covering the runs it is for. Both now ask layerwise_export_requested. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test aa15aa6 |
Note
The expert-parallel PTQ series is now a tree instead of one stack (re-cut 2026-10-27, from main 1521053), so PRs that do not depend on each other can be reviewed and merged in parallel. This PR was [21/21]. Rebased onto main 7c3c868 later the same day, after #2739 merged, and onto main f03651e that night.
Base:
shengliangx/hfep-base-14-hf-ptq, a branch (not a PR) that merges #2746 and #2753, the PRs this one builds on. The diff below is this PR's alone, and it merges after all of them.Re-cut: this PR's diff is unchanged; only its base moved. Review fixes since are listed at the end of Testing.
Series layout
mainWhat does this PR do?
Type of change: New feature
hf_ptq.pyhad one distributed path,--use_fsdp2, which shards a dense model and replicates every expert of an MoE. This PR wires the expert-parallel path (the rest of the series; see the layout above) into it, so the documented CLI reaches an expert-parallel run: recipes, export formats, deprecations.main()runs insidedistributed_run([OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736), which owns the process group on a distributed launch. The run takes this process'sDistributedState(rank, size, device) from it, with--devicefor a single process. FSDP2 on one rank still shards through a process group, of one, which the load sets up ([OMNIML-6054] Route load_pretrained by the distribution plan #2779) anddistributed_rundestroys with the run. The example's ownDistributedState,setup_distributed_argsand process-group helpers are gone.load_modeldecides the plan once, withresolve_distribution_plan([OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737, [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780, [OMNIML-6076] Describe expert parallelism in the model specs #2746), and loads every model by it throughload_pretrained([OMNIML-6054] Route load_pretrained by the distribution plan #2779): expert-parallel, FSDP2,get_model's whole-model loads and the low-memory load.load_pretrainedrecords the plan, so the script records none itself: [OMNIML-6071] Calibrate a distributed model on per-rank data #2740's record after the--use_fsdp2load is gone. Everything after reads the plan back from the model, not the flag or the process group: the calibration device, the data sharding, the calibration loop, andget_model's GPU placement.MODELOPT_EP_DEGREEandMODELOPT_FSDP_SIZEoverride the automatic choices.--use_fsdp2asks the decision for FSDP2 and no expert parallelism.--use_fsdp2, the flag this script's FSDP2 checks hang on, which now run before anything is set up.resolve_distribution_planrefuses a plan that would quietly replicate an expert-parallel-capable MoE, and a model whose spec refuses expert parallelism (GPT-OSS, Llama-4, [OMNIML-6076] Describe expert parallelism in the model specs #2746) raises unless--use_fsdp2rules expert parallelism out. Left to decide, the plan replicates a dense part it cannot FSDP2-shard within several expert-parallel groups, which the export cannot write yet; asking for that layout raises before loading and names the settings that avoid it. An AutoQuantize recipe raises on the expert-parallel and replicated routes, since its search scores each rank's shard without reducing the scores;--use_fsdp2keeps main's warning.--low_memory_mode,--use_seq_device_mapand--offload_folder(with it--max_cpu_memory_gb/--max_gpu_memory_gb) configure a single process's load, so an expert-parallel or replicated run refuses them before loading: an expert-parallel run never reaches those loaders, and a replicated one pins each rank's copy to its GPU, which they cannot work with. A single process and--use_fsdp2keep main's handling. An expert-parallel run also refuses--vllm_fakequant_exportbefore loading, since that exporter writes the whole model from each process it runs in; a replicated run keeps it (rank 0 exports), and--use_fsdp2keeps its parsing-time refusal.config.json. A multimodal model's source config is rank 0's in every plan. A speculative-decoding model's export, which writes the whole checkpoint from one process, is refused on every rank when the model is sharded.Every distributed run calibrates on its own 1/world shard of the data ([OMNIML-6071] Calibrate a distributed model on per-rank data #2740), the offline speculative-decoding loader included; a loader of a kind that cannot be split is refused.
Expert-parallel runs go through
distributed_forward_loop, which re-plants the all-to-all forwards that quantizer insertion clobbers on transformers 5.16 and later. From transformers 5.16, quantizer insertion drops the routed experts' all-to-all forward wrapper, and a recipe whose calibration runs no loop (configs that need no calibration, such asint8_weight_onlyorw4a16_nvfp4, and the MX presets, whose algorithm isNone) would leave it dropped, so the preview after PTQ would fail before the export. Once quantized, an expert-parallel run therefore re-plants it. On 5.15 and earlier the TP transforms are forward hooks that insertion leaves in place, and the re-plant finds nothing to restore.An expert-parallel VLM calibrated on text runs its batches through the language model, but the loop re-plants the all-to-all forwards over the whole model: transformers keeps the expert-parallel plan on the model
from_pretrainedbuilt, and from 5.16 a VLM's language model reports none of its own, so a loop handed the language model would re-plant nothing, and the first batch would raise for the missing EP process group. The--qformatpath of an expert-parallel VLM also needs [OMNIML-6071] Calibrate a distributed model on per-rank data #2740's guard fix, which lets a module with no routed experts (the vision tower the script quantizes with every quantizer off) pass the expert-parallel calibration check.FSDP2 keeps main's plain loop, with its split-on-OOM retry.
The calibration algorithms under a plan that spans several ranks:
max(amax all-reduced over the data-, expert- and tensor-parallel groups),mse(searches the weights max leaves identical on every rank)gptq(Hessians merged across each module's data-parallel replicas),smoothquant(scales from max's synced amax),nvfp4_act_headroom(activation scales MAX-reduced over data parallelism)awq_lite,awq_clip,awq_full,svdquant,local_hessian,imatrix, andmaxormsewithdistributed_syncoff, also as another algorithm's weight or scale algorithm: each keeps its statistics on its rank, so the ranks' copies of a weight would come out differentlsq's learnable scales (its first scale calibration follows the rows above)synced_gpuswhen the model's plan shards it. Under expert parallelism a preview that fails raises, which aborts the run, rather than leaving the other ranks in the next step's collective; its error says the export was not reached and names--skip_generate.--use_fsdp2keeps main's best-effort skip and still exports, which is safe for a failure every rank meets at the same step. A failure on one rank alone leaves the others ingenerate's collectives until the collective timeout. While a preview decodes,generateis kept from switching a MoE's experts tobatched_mm, which bypasses the quantizers, on every module of the model that has the decode hook: a model can generate through a submodule, as a Nemotron-VL model'schatgenerates through its language model. The guard puts back the hook each module held itself, so a hook bound per instance survives and previews nest. The ranks also decide together whether to run Nemotron-VL's image preview: each node reads the sample images from its own checkout, and the repository ships none.--use_seq_device_map, which spreads one model over every GPU, and--offload_folderare refused before loading there. Under a plan with several ranks, a device that names no GPU index is refused before loading, rather than left to accelerate's "auto". A pinned copy is not sized against the node's GPUs, a limit its one-device map would ignore. A single process keeps spreading a model too big for one GPU, as before.--use_fsdp2for their released MXFP4 / multimodal checkpoints) quantize in a single process.launch_memory_monitorprobed NVML before initializing it. Every probe raisedUninitialized, so the per-GPU peak-memory report never started on any host where nothing else had initialized NVML. The probe now initializes NVML itself, and only rank 0 starts the monitor, which reads every GPU on its node.Usage
# One node, 8 GPUs: the experts shard over all 8 ranks, each calibrating its own data. torchrun --nproc_per_node 8 examples/hf_ptq/hf_ptq.py \ --pyt_ckpt_path Qwen/Qwen3-30B-A3B --qformat nvfp4 --export_path /out/qwen3-30b-a3b-nvfp4Testing
tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py(70 cases; the decision itself is tested in [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737, [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 and [OMNIML-6076] Describe expert parallelism in the model specs #2746):--use_fsdp2, and FSDP2 sharding refused unless--use_fsdp2;load_modelloading by the plan: an EP plan and--use_fsdp2throughload_pretrainedand no other loader, a data-parallel one throughget_model;--low_memory_mode,--use_seq_device_map,--offload_folder) refused on the expert-parallel and replicated routes and kept by a single process;main()running insidedistributed_run, which it leaves even when the run raises, and handing the run the statedistributed_runyields, with--devicefor a single process;synced_gpusexactly when the plan shards the model (EP or FSDP2 over several ranks);None, previews on): on transformers 5.18 the run without the re-plant failed in the post-quantization preview on both ranks ("torch_a2a experts forward requires the EP process_group") and wrote no checkpoint, and with it the run completed, restoring 8 forward wrappers, and exported; on transformers 5.9.0, where there is nothing to re-plant (up to 5.15 the TP transforms are hooks that quantizer insertion leaves alone), both runs exported byte-identical files;test_ptq_qwen3_moe_expert_parallel(tests/examples/hf_ptq/test_llm_ptq.py):torchrun --nproc_per_node 2 hf_ptq.pywithMODELOPT_EP_DEGREE=2on a tiny Qwen3-MoE and the FP8 recipe. It checks that every expert of both ranks is exported FP8 with its weight scale, and that the config names FP8. It is the tests' one real expert-parallel run of the script; the file above covers the wiring with stand-ins. It passed on 2 GB200s in 29.5 s at f4bb395488 (2026-10-16), which carries this test on a revision before the latest loader and calibration fixes ([OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739, [OMNIML-6071] Calibrate a distributed model on per-rank data #2740). It skips on CI's 1-GPU PR runner and runs on 2-GPU runners.tests/examples/hf_ptq/test_hf_ptq_model_type.py(main's): the preview tests' stand-in models are modules now, since the previews read their distribution plan.tests/examples/hf_ptq/test_example_utils.py: the preview keepinggeneratefrom switching the experts implementation, also on a language model that a model around it generates through (fails without the fix), a model without the decode hook previewed as it is, and a decode hook the model holds itself put back, also when previews nest.tests/unit/torch/utils/test_memory_monitor.py, against a fake reference-counted NVML: the monitor starts when NVML can report memory (this fails on the old code), and is skipped with NVML shut down when it cannot.tests/examples/hf_ptq/test_hf_ptq_args.py: the memory monitor started on rank 0 only.get_model's tests cover are dropped, and the load-call, calibration-loop and memory-monitor pairs are one parametrized test each. Each drop is checked by mutation.test_hf_ptq_expert_parallel.pyalso covers: the pin on the pack-quantized, MXFP4 and DiffusionGemma load paths and the sequential-map refusal; a pinned copy not held to a node-wide memory limit; the offline speculative-decoding loader split across ranks; a failed preview raising on an expert-parallel model and skipped, with the export still run, on a replicated or--use_fsdp2one; a device that names no GPU index refused where each rank holds its own copy; and an AutoQuantize recipe refused on the routes this script adds.test_hf_ptq_expert_parallel.pyskip below transformers 5.7, where [OMNIML-6076] Describe expert parallelism in the model specs #2746 refuses expert parallelism before the route they exercise; their single-process and--use_fsdp2cases still run there.tests/uniton transformers 5.18 and 5.5 and on torch 2.8, and the hf_ptq example tests, each with the same failures as main in that environment. CI runs the full unit suite.load_pretrained([OMNIML-6054] Route load_pretrained by the distribution plan #2779, [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780); this revision's GPU tests run in CI only. The series' GPU tests on this branch: 19 passed on a 4-GPU node. On 1 GPU, CI's PR runner, 6 passed and 13 skipped. They cover:nox -s gpu(all oftests/gpu, 1 GPU, CI's PR-runner shape): every test failing on this branch also fails at the base, and the only new tests are the series' own.tests/examples/hf_ptq, TensorRT-LLM image, 1 GPU): no outcome changes on the 296 shared tests. The series' 30 pass, except 2 that need transformers 5.7.tests/gpu_megatron(2 GPUs): no outcome changes over the 311 tests both runs reached. The rest time out identically at the base in that image.Qwen3-30B-A3B at EP16 over 4 nodes, NVFP4 and FP8: the exports with the dense part FSDP-sharded over the world and with it replicated have byte-identical weights and weight scales; only calibration-derived input scales differ. Every dense FP8 weight is FP8 with its scale, and no NVFP4 weight has the all-zero high nibbles of a lost pack shift.
Qwen3-VL-30B and MiniMax-M3: tensor counts match single-GPU exports, and every expert weight is packed.
MiniMax-M2.7 (block-FP8 source) at EP16 over 4 nodes, NVFP4: every exported weight name is the source checkpoint's, and routed experts are
block_sparse_moe.experts.N.w1/w2/w3. Served by vLLM (0.30 dev build, TP4), it scores 0.94 / 0.93 on gsm8k (5-shot, 100 samples, flexible / strict), against the FP8 source's 0.93 / 0.95.DeepSeek-V3 and GLM: on an earlier revision.
Kimi-K3 NVFP4 at EP64 over 16 GB200 nodes, in 10 minutes (peak 155 GiB per GPU on an earlier build of this series), checked against the source checkpoint:
The same checks flag the export made before this series' fixes. vLLM 0.28 loads the export and starts serving, but it cannot serve a Kimi-K3 NVFP4 export with an FP8 KV cache: its FP8-KV path needs an FP8 prefill query, which its NVFP4 linears reject. TBD: generation and gsm8k from the checkpoint-mirror recipe
models/moonshotai/Kimi-K3/ptq/nvfp4_experts-fp8_pb_attentionon this series' code with the [OMNIML-6073] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 fix.load_pretrained, which this PR loads every distributed plan through, and an import that only the removed refusal test used is gone.--use_fsdp2's checks (validate_fsdp2_supported) run on the checkpoint's config before the plan is decided, as they ran before this PR, so a checkpoint they refuse (VILA, multimodal, pack-quantized) gets their message instead of the meta-device build's error. The new test fails without the change.layerwise_export_requested, decides both the pre-load refusal and the layerwise export switch; a comment names the load paths without a device map (Nemotron VL), where each rank builds a whole host copy.Before your PR is "Ready for review"
--use_fsdp2runs behave as before, except that under torchrun a--deviceother thancudais now refused (bydistributed_run, [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736), where main's--use_fsdp2ignored it and ran each rank oncuda:<local_rank>. A torchrun launch without--use_fsdp2now runs an MoE expert-parallel.CONTRIBUTING.md: N/AAdditional Information
Part 21 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after #2749.
Summary by CodeRabbit